CLUSTERING

In [1]:
from pathlib import Path

# définir le répertoire du projet contenant le dossier data/ et notebooks/
HOME = Path.cwd().parent
print(f"Home directory: {HOME}")

# définir le répertoire des données
DATA = Path(HOME, "data")
print(f"Data directory: {DATA}")
Home directory: C:\Users\DELL\Desktop\Projet ML2
Data directory: C:\Users\DELL\Desktop\Projet ML2\data
In [3]:
! pip install pywaffle
Collecting pywaffle
  Downloading pywaffle-1.1.0-py2.py3-none-any.whl (30 kB)
Collecting fontawesomefree (from pywaffle)
  Downloading fontawesomefree-6.5.1-py3-none-any.whl (25.6 MB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 25.6/25.6 MB 47.7 MB/s eta 0:00:00
Requirement already satisfied: matplotlib in /usr/local/lib/python3.10/dist-packages (from pywaffle) (3.7.1)
Requirement already satisfied: contourpy>=1.0.1 in /usr/local/lib/python3.10/dist-packages (from matplotlib->pywaffle) (1.2.1)
Requirement already satisfied: cycler>=0.10 in /usr/local/lib/python3.10/dist-packages (from matplotlib->pywaffle) (0.12.1)
Requirement already satisfied: fonttools>=4.22.0 in /usr/local/lib/python3.10/dist-packages (from matplotlib->pywaffle) (4.53.0)
Requirement already satisfied: kiwisolver>=1.0.1 in /usr/local/lib/python3.10/dist-packages (from matplotlib->pywaffle) (1.4.5)
Requirement already satisfied: numpy>=1.20 in /usr/local/lib/python3.10/dist-packages (from matplotlib->pywaffle) (1.25.2)
Requirement already satisfied: packaging>=20.0 in /usr/local/lib/python3.10/dist-packages (from matplotlib->pywaffle) (24.0)
Requirement already satisfied: pillow>=6.2.0 in /usr/local/lib/python3.10/dist-packages (from matplotlib->pywaffle) (9.4.0)
Requirement already satisfied: pyparsing>=2.3.1 in /usr/local/lib/python3.10/dist-packages (from matplotlib->pywaffle) (3.1.2)
Requirement already satisfied: python-dateutil>=2.7 in /usr/local/lib/python3.10/dist-packages (from matplotlib->pywaffle) (2.8.2)
Requirement already satisfied: six>=1.5 in /usr/local/lib/python3.10/dist-packages (from python-dateutil>=2.7->matplotlib->pywaffle) (1.16.0)
Installing collected packages: fontawesomefree, pywaffle
Successfully installed fontawesomefree-6.5.1 pywaffle-1.1.0
In [5]:
! pip install category_encoders
Collecting category_encoders
  Downloading category_encoders-2.6.3-py2.py3-none-any.whl (81 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 81.9/81.9 kB 952.6 kB/s eta 0:00:00
Requirement already satisfied: numpy>=1.14.0 in /usr/local/lib/python3.10/dist-packages (from category_encoders) (1.25.2)
Requirement already satisfied: scikit-learn>=0.20.0 in /usr/local/lib/python3.10/dist-packages (from category_encoders) (1.2.2)
Requirement already satisfied: scipy>=1.0.0 in /usr/local/lib/python3.10/dist-packages (from category_encoders) (1.11.4)
Requirement already satisfied: statsmodels>=0.9.0 in /usr/local/lib/python3.10/dist-packages (from category_encoders) (0.14.2)
Requirement already satisfied: pandas>=1.0.5 in /usr/local/lib/python3.10/dist-packages (from category_encoders) (2.0.3)
Requirement already satisfied: patsy>=0.5.1 in /usr/local/lib/python3.10/dist-packages (from category_encoders) (0.5.6)
Requirement already satisfied: python-dateutil>=2.8.2 in /usr/local/lib/python3.10/dist-packages (from pandas>=1.0.5->category_encoders) (2.8.2)
Requirement already satisfied: pytz>=2020.1 in /usr/local/lib/python3.10/dist-packages (from pandas>=1.0.5->category_encoders) (2023.4)
Requirement already satisfied: tzdata>=2022.1 in /usr/local/lib/python3.10/dist-packages (from pandas>=1.0.5->category_encoders) (2024.1)
Requirement already satisfied: six in /usr/local/lib/python3.10/dist-packages (from patsy>=0.5.1->category_encoders) (1.16.0)
Requirement already satisfied: joblib>=1.1.1 in /usr/local/lib/python3.10/dist-packages (from scikit-learn>=0.20.0->category_encoders) (1.4.2)
Requirement already satisfied: threadpoolctl>=2.0.0 in /usr/local/lib/python3.10/dist-packages (from scikit-learn>=0.20.0->category_encoders) (3.5.0)
Requirement already satisfied: packaging>=21.3 in /usr/local/lib/python3.10/dist-packages (from statsmodels>=0.9.0->category_encoders) (24.0)
Installing collected packages: category_encoders
Successfully installed category_encoders-2.6.3
In [2]:
%pylab inline
import pandas as pd
import seaborn as sns
import plotly.graph_objs as go
from pywaffle import Waffle
from sklearn.base import clone
from sklearn.pipeline import Pipeline
from sklearn.cluster import KMeans
from sklearn.preprocessing import MinMaxScaler, StandardScaler
from sklearn.metrics import silhouette_score, adjusted_rand_score
import category_encoders as ce
from sklearn.decomposition import PCA
from time import time
from yellowbrick.cluster import KElbowVisualizer
from yellowbrick.cluster import silhouette_visualizer
import operator
from sklearn import manifold
import plotly.express as px
from sklearn.mixture import GaussianMixture
import datetime as dt
from dateutil.relativedelta import relativedelta

import warnings
warnings.filterwarnings('ignore')

pd.set_option('display.max_rows', 100)
pd.set_option('display.max_columns', 200)
plt.style.use('fivethirtyeight')
%pylab is deprecated, use %matplotlib inline and import the required libraries.
Populating the interactive namespace from numpy and matplotlib
In [3]:
db_df = pd.read_csv(Path(DATA, "data.csv"), sep=",")
db_df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 119143 entries, 0 to 119142
Data columns (total 43 columns):
 #   Column                         Non-Null Count   Dtype  
---  ------                         --------------   -----  
 0   customer_id                    119143 non-null  object 
 1   customer_unique_id             119143 non-null  object 
 2   customer_zip_code_prefix       119143 non-null  int64  
 3   customer_city                  119143 non-null  object 
 4   customer_state                 119143 non-null  object 
 5   order_id                       119143 non-null  object 
 6   order_status                   119143 non-null  object 
 7   order_purchase_timestamp       119143 non-null  object 
 8   order_approved_at              118966 non-null  object 
 9   order_delivered_carrier_date   117057 non-null  object 
 10  order_delivered_customer_date  115722 non-null  object 
 11  order_estimated_delivery_date  119143 non-null  object 
 12  delivery_time                  115722 non-null  float64
 13  delivery_delay                 115722 non-null  float64
 14  order_item_id                  118310 non-null  float64
 15  product_id                     118310 non-null  object 
 16  seller_id                      118310 non-null  object 
 17  shipping_limit_date            118310 non-null  object 
 18  price                          118310 non-null  float64
 19  freight_value                  118310 non-null  float64
 20  product_category_name          118310 non-null  object 
 21  product_name_lenght            118310 non-null  float64
 22  product_description_lenght     118310 non-null  float64
 23  product_photos_qty             118310 non-null  float64
 24  product_weight_g               118290 non-null  float64
 25  product_length_cm              118290 non-null  float64
 26  product_height_cm              118290 non-null  float64
 27  product_width_cm               118290 non-null  float64
 28  seller_zip_code_prefix         118310 non-null  float64
 29  seller_city                    118310 non-null  object 
 30  seller_state                   118310 non-null  object 
 31  review_id                      118146 non-null  object 
 32  review_score                   118146 non-null  float64
 33  review_comment_title           13989 non-null   object 
 34  review_comment_message         50244 non-null   object 
 35  review_creation_date           118146 non-null  object 
 36  review_answer_timestamp        118146 non-null  object 
 37  payment_sequential             119140 non-null  float64
 38  payment_type                   119140 non-null  object 
 39  payment_installments           119140 non-null  float64
 40  payment_value                  119140 non-null  float64
 41  product_category_name_english  118310 non-null  object 
 42  product_category_agg           116576 non-null  object 
dtypes: float64(17), int64(1), object(25)
memory usage: 39.1+ MB

Computing final features¶

In [4]:
def mode_w_nan(series):
    if all(([x is np.nan for x in series])):
        return 'unknown'
    return pd.Series.mode(series)[0]  # if multiple items are returned, take the first


def compute_features(db_df):
    '''Compute the features of the final dataset, same way as the previous notebook (without correlated features and categorical features with too many unique values)
    db_df: DataFrame - Grouped tables
    output: Dataframe - Final dataset
    '''
    for col in db_df.columns:
        # dates as DateTime
        if any([sub in col for sub in ['date', '_at', 'timestamp']]):
            # Try to infer the datetime format automatically
            db_df[col] = pd.to_datetime(db_df[col], errors='coerce')
    # number of orders per customer
    df = pd.DataFrame(data={'nb_orders': db_df.groupby('customer_unique_id')['customer_id'].nunique(dropna=True)})
    # total number of products ordered per customer
    df['total_products_ordered'] = db_df.groupby(['customer_unique_id', 'order_id'])['order_item_id'].max().groupby('customer_unique_id').sum()
    # total paid
    df['total_paid'] = db_df.groupby('customer_unique_id')['price'].sum()
    # mean description length for ordered products
    # df['mean_prod_descr_length'] = db_df.groupby('customer_unique_id')['product_description_lenght'].mean()
    # mean photos qty for ordered products
    df['mean_photo_qty_for_ordered_prods'] = db_df.groupby('customer_unique_id')['product_photos_qty'].mean()
    # mean review score
    df['mean_reviews_score'] = db_df.groupby('customer_unique_id')['review_score'].mean()
    # first order date
    first_order_date = db_df.groupby('customer_unique_id')['order_purchase_timestamp'].min()

    # last order date
    last_order_date = db_df.groupby('customer_unique_id')['order_purchase_timestamp'].max()
    # days since the last order
    df['recency'] = pd.to_timedelta(db_df['order_purchase_timestamp'].max() - last_order_date).dt.days
    # mean delivery time
    df['mean_delivery_time'] = db_df.groupby('customer_unique_id')['delivery_time'].mean(numeric_only=False)
    # mean delivery delay
    # df['mean_delivery_delay'] = db_df.groupby('customer_unique_id')['delivery_delay'].mean(numeric_only=False)
    # favorite product category
    df['favorite_category'] = db_df.groupby('customer_unique_id')['product_category_agg'].agg(mode_w_nan)
    # favorite payment type
    df['favorite_payment_type'] = db_df.groupby('customer_unique_id')['payment_type'].agg(mode_w_nan)
    # customer state
    df = df.join(db_df[['customer_unique_id', 'customer_state']].drop_duplicates(subset=['customer_unique_id']).set_index('customer_unique_id'))
    # fill null values caused when trying to divide by 0
    for col in df.columns:
        if 'mean' in col:
            df[col].fillna(0, inplace=True)
    return df
In [5]:
df = compute_features(db_df)
df.info()
<class 'pandas.core.frame.DataFrame'>
Index: 96096 entries, 0000366f3b9a7992bf8c76cfdf3221e2 to ffffd2657e2aad2907e67c3e9daecbeb
Data columns (total 10 columns):
 #   Column                            Non-Null Count  Dtype  
---  ------                            --------------  -----  
 0   nb_orders                         96096 non-null  int64  
 1   total_products_ordered            96096 non-null  float64
 2   total_paid                        96096 non-null  float64
 3   mean_photo_qty_for_ordered_prods  96096 non-null  float64
 4   mean_reviews_score                96096 non-null  float64
 5   recency                           96096 non-null  int64  
 6   mean_delivery_time                96096 non-null  float64
 7   favorite_category                 96096 non-null  object 
 8   favorite_payment_type             96096 non-null  object 
 9   customer_state                    96096 non-null  object 
dtypes: float64(5), int64(2), object(3)
memory usage: 8.1+ MB

RFM¶

In [7]:
def rfm_iso_scatter(data, x, y, z, color='nb_orders', reduce=True):
    '''generate a 3D scatter plot
    Input:
    - dat : dataframe
    - x, y, z : str - 3 features name for the three coordinates
    '''
    if reduce:
        x = data[x]
        y = data[data[y] < 10][y]
        z = data[data[z] < 4000][z]
    else:
        x = data[x]
        y = data[y]
        z = data[z]
        

    fig = go.Figure(data=[go.Scatter3d(x=x, y=y, z=z, mode='markers', marker=dict(size=1, color=data[color], colorscale='thermal', opacity=0.8))])

    fig.update_layout(scene=dict(
        xaxis_title='Recency',
        yaxis_title='Frequency',
        zaxis_title='Monetary'),
        width=800,
        margin=dict(r=20, b=10, l=10, t=10))
    fig.update_layout(margin=dict(l=0, r=0, b=0, t=0))
    fig.show()
In [8]:
rfm_iso_scatter(df, 'recency', 'nb_orders', 'total_paid')

I. K-MEANS

A. Prepocessing

1. Encoding & Scaling

In [9]:
categorical_cols = df.columns[df.dtypes == object].tolist()

preprocessor = Pipeline([
    ('encoding', ce.OneHotEncoder(cols=categorical_cols)),
    ('features_scaling', MinMaxScaler())  # MinMaxScaler because the shape of all features do not follows a normal distribution
])
In [10]:
data_scaled = preprocessor['encoding'].fit_transform(df)

2. PCA

In [11]:
n_components = min(data_scaled.shape[0], data_scaled.shape[1])
# addind a PCA step in the preprocessor pipeline
preprocessor.steps.append(['pca', PCA(n_components=n_components, random_state=0)])
In [12]:
preprocessor.fit(df)
Out[12]:
Pipeline(steps=[('encoding',
                 OneHotEncoder(cols=['favorite_category',
                                     'favorite_payment_type',
                                     'customer_state'])),
                ('features_scaling', MinMaxScaler()),
                ['pca', PCA(n_components=52, random_state=0)]])
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
Pipeline(steps=[('encoding',
                 OneHotEncoder(cols=['favorite_category',
                                     'favorite_payment_type',
                                     'customer_state'])),
                ('features_scaling', MinMaxScaler()),
                ['pca', PCA(n_components=52, random_state=0)]])
OneHotEncoder(cols=['favorite_category', 'favorite_payment_type',
                    'customer_state'])
MinMaxScaler()
PCA(n_components=52, random_state=0)
In [14]:
fig, ax = plt.subplots(figsize=(12, 7))
plt.plot(np.arange(0, n_components), preprocessor['pca'].explained_variance_ratio_.cumsum())
plt.title("Explained variance vs. number of components", fontsize=16)
plt.ylabel("Cumsum explained variance ratio")
plt.xlabel("Factor number")
plt.xticks(range(0, n_components+1, 5))
plt.show()
No description has been provided for this image

Plus de 90% de la variance a été expliquée par 26 composants

In [13]:
# Assuming 'df' has 26 or fewer features, set n_components to a value less than or equal to 26
preprocessor['pca'].n_components = 26

data_pca = preprocessor.fit_transform(df)
data_pca.shape
Out[13]:
(96096, 26)
In [14]:
data_pca
Out[14]:
array([[-0.34100803,  0.71395258,  0.71899138, ...,  0.00116628,
        -0.01011132,  0.00599096],
       [-0.31724919,  0.60822985, -0.30847366, ..., -0.01193224,
        -0.01172388, -0.00349919],
       [-0.28909382, -0.38698585, -0.05458112, ..., -0.05797138,
         0.0198105 , -0.04046975],
       ...,
       [-0.29688053, -0.33439444, -0.07436493, ...,  0.06207249,
         0.02535949,  0.12949427],
       [-0.31456381, -0.34651048, -0.0973208 , ..., -0.22907343,
        -0.01619359, -0.11797202],
       [-0.30970875, -0.38453026, -0.2217024 , ..., -0.03247049,
         0.01727301, -0.02586894]])
In [17]:
# Using 2 first components of the PCA
plt.figure(figsize=(10, 7))
plt.scatter(data_pca[:, 0], data_pca[:, 1], s=30)
plt.xlabel("PCA 1")
plt.ylabel("PCA 2")
plt.show()
No description has been provided for this image

3. Nombre optimal de Cluster

In [15]:
kmeans_pipe = Pipeline([
    ('preprocessor', preprocessor),
    ('clusterer', KMeans())
])
kmeans_pipe
Out[15]:
Pipeline(steps=[('preprocessor',
                 Pipeline(steps=[('encoding',
                                  OneHotEncoder(cols=['favorite_category',
                                                      'favorite_payment_type',
                                                      'customer_state'])),
                                 ('features_scaling', MinMaxScaler()),
                                 ['pca',
                                  PCA(n_components=26, random_state=0)]])),
                ('clusterer', KMeans())])
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
Pipeline(steps=[('preprocessor',
                 Pipeline(steps=[('encoding',
                                  OneHotEncoder(cols=['favorite_category',
                                                      'favorite_payment_type',
                                                      'customer_state'])),
                                 ('features_scaling', MinMaxScaler()),
                                 ['pca',
                                  PCA(n_components=26, random_state=0)]])),
                ('clusterer', KMeans())])
Pipeline(steps=[('encoding',
                 OneHotEncoder(cols=['favorite_category',
                                     'favorite_payment_type',
                                     'customer_state'])),
                ('features_scaling', MinMaxScaler()),
                ['pca', PCA(n_components=26, random_state=0)]])
OneHotEncoder(cols=['favorite_category', 'favorite_payment_type',
                    'customer_state'])
MinMaxScaler()
PCA(n_components=26, random_state=0)
KMeans()
In [16]:
def display_kmeans_results(pipeline, data):
    t0 = time()
    pipeline.fit(data)
    fit_time = time() - t0

    results = [kmeans_pipe['clusterer'].init, fit_time, kmeans_pipe['clusterer'].n_iter_, kmeans_pipe['clusterer'].inertia_]

    kmeans_data_pca = kmeans_pipe['preprocessor'].transform(df)
    kmeans_pred_labels = kmeans_pipe['clusterer'].labels_

    results += [silhouette_score(kmeans_data_pca, kmeans_pred_labels, metric='euclidean')]
    print('init\t\ttime\tnb_iter\tinertia\tsilhouette')
    print("{:9s}\t{:.3f}s\t{}\t{:.0f}\t{:.3f}".format(*results))

La fonction display_kmeans_results entraîne un pipeline KMeans sur un jeu de données, mesure le temps d'entraînement, et collecte des métriques de performance clés telles que la méthode d'initialisation, le nombre d'itérations jusqu'à la convergence, l'inertie et le score silhouette. Elle transforme les données, prédit les labels des clusters, et affiche ces résultats de manière formatée pour évaluer la qualité et l'efficacité du clustering réalisé.

In [20]:
display_kmeans_results(kmeans_pipe, df)
init		time	nb_iter	inertia	silhouette
k-means++	7.121s	8	115375	0.200

Elbow method

In [17]:
def display_elbow_visu(pipeline, data, k_range, metric, elbow=True):

    pipeline.fit(data)
    (fig, ax) = plt.subplots(figsize=(10, 7))
    visualizer_WCSS = KElbowVisualizer(clone(pipeline['clusterer']), k=k_range, metric=metric, locate_elbow=elbow, ax=ax)
    visualizer_WCSS.fit(pipeline['preprocessor'].fit_transform(data))
    visualizer_WCSS.show()
In [32]:
k_range = (2, 11)
display_elbow_visu(kmeans_pipe, df, k_range, 'distortion')
No description has been provided for this image

Le score de distorsion $ W(C) $ pour un ensemble de clusters $ C = \{C_1, C_2, \ldots, C_k\} $ avec leurs centroides respectifs $ \mu_1, \mu_2, \ldots, \mu_k $ est défini comme :

$ W(C) = \sum_{i=1}^k \sum_{x \in C_i} \| x - \mu_i \|^2 $

  • $ C_i $ représente le i-ème cluster.

  • $ x $ est un point de données dans le cluster $ C_i $.

  • $ \mu_i $ est le centroïde du cluster $ C_i $.

  • $ \| x - \mu_i \|^2 $ est la distance euclidienne au carré entre le point $ x $ et le centroïde $ \mu_i $.

Cette formule quantifie la somme des distances au carré de tous les points de données à leurs centroides respectifs dans leurs clusters assignés.

In [34]:
%%time
display_elbow_visu(kmeans_pipe, df, k_range, 'silhouette', elbow=False)
No description has been provided for this image
CPU times: total: 31min 16s
Wall time: 21min 13s

Analyse de la silhouette

In [18]:
def silouhette_analysis(pipeline, data, k_range):

    scores_dict = {}
    for i in range(k_range[0], k_range[1]):
        pipeline['clusterer'].n_clusters = i
        (fig, ax) = plt.subplots()
        visu = silhouette_visualizer(clone(pipeline['clusterer']), pipeline['preprocessor'].fit_transform(data), colors='yellowbrick', ax=ax)
        scores_dict[i] = visu.silhouette_score_  # index = number of clusters / value = silouette score
    for item in scores_dict:
        print('{} clusters - Average silhouette score : {}'.format(item, scores_dict[item]))
    return scores_dict
In [36]:
%%time
scores = silouhette_analysis(kmeans_pipe, df, (4, 11))
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
4 clusters - Average silhouette score : 0.1584443451685507
5 clusters - Average silhouette score : 0.1840502897531775
6 clusters - Average silhouette score : 0.1761567551139147
7 clusters - Average silhouette score : 0.21194686181308134
8 clusters - Average silhouette score : 0.19908050027228946
9 clusters - Average silhouette score : 0.2025438876158195
10 clusters - Average silhouette score : 0.21510648809841731
CPU times: total: 48min 42s
Wall time: 39min 17s

La méthode Elbow nous permet de choisir k = 5

In [38]:
best_sil_score = max(scores.items(), key=operator.itemgetter(1))[0]
print('Best average silhouette score : {} clusters ({:.4})'.format(best_sil_score, scores[best_sil_score]))
Best average silhouette score : 10 clusters (0.2151)
In [39]:
# set the number of clusters which produce the best average silouhette score
kmeans_pipe['clusterer'].n_clusters = 5

4. Visualisation

PCA reduced data

In [41]:
def visualize_kmeans_clustering(pipeline, data, pca=False):
    reduced_data = pipeline['preprocessor'].fit_transform(data)
    if pca:
        reduced_data = PCA(n_components=2).fit_transform(reduced_data)
    pipeline['clusterer'].fit(reduced_data[:, :2])

    # Step size of the mesh. Decrease to increase the quality of the VQ.
    h = .02   # point in the mesh [x_min, x_max]x[y_min, y_max].

    # Plot the decision boundary. For that, we will assign a color to each
    x_min, x_max = reduced_data[:, 0].min() - 1, reduced_data[:, 0].max() + 1
    y_min, y_max = reduced_data[:, 1].min() - 1, reduced_data[:, 1].max() + 1
    xx, yy = np.meshgrid(np.arange(x_min, x_max, h), np.arange(y_min, y_max, h))

    # Obtain labels for each point in mesh.
    Z = pipeline['clusterer'].predict(np.c_[xx.ravel(), yy.ravel()])

    # Put the result into a color plot
    Z = Z.reshape(xx.shape)
    plt.figure(1, figsize=(15, 12))
    plt.clf()
    plt.imshow(Z, interpolation="nearest",
               extent=(xx.min(), xx.max(), yy.min(), yy.max()),
               cmap=plt.cm.Paired, aspect="auto", origin="lower")

    plt.plot(reduced_data[:, 0], reduced_data[:, 1], 'k.', markersize=2)
    # Plot the centroids as a white X
    centroids = pipeline['clusterer'].cluster_centers_
    plt.scatter(centroids[:, 0], centroids[:, 1], marker="x", s=50, linewidths=2, color="w", zorder=10)
    plt.title("K-means clustering on the OLIST dataset (PCA-Reduced)\nWith {} clusters".format(pipeline['clusterer'].n_clusters))
    plt.xlim(x_min, x_max)
    plt.ylim(y_min, y_max)
    plt.xticks(())
    plt.yticks(())
    plt.show()

Ce fonction entraîne un pipeline de clustering K-means sur des données réduites via un préprocesseur (et éventuellement via une analyse en composantes principales, PCA), puis visualise les frontières de décision des clusters sur un graphique 2D. Il transforme les données d'entrée en un espace réduit à deux dimensions, ajuste le modèle K-means, et génère une grille de points couvrant cet espace. Il prédit les labels des clusters pour chaque point de la grille et affiche les résultats sous forme de carte de couleur. Les points d'origine des données sont tracés en noir et les centroïdes des clusters sont indiqués par des croix blanches. Le graphique est annoté avec le nombre de clusters utilisés et montre les limites des clusters déterminées par l'algorithme K-means.

In [42]:
visualize_kmeans_clustering(kmeans_pipe, df)
No description has been provided for this image

T-SNE

Cette fonction réalise une visualisation des clusters en utilisant la réduction de dimension avec l'algorithme t-SNE. Il transforme les données en deux dimensions pour permettre une visualisation 2D des clusters, en utilisant les paramètres définis dans tsne_params. Les résultats de cette transformation sont stockés dans un DataFrame, où chaque point est coloré selon son étiquette de cluster prédite. Une figure est ensuite générée avec seaborn, montrant un graphique de dispersion des points, ce qui permet de visualiser la répartition et la séparation des clusters dans l'espace réduit.

In [21]:
tsne_params = {
    'n_components': 2,
    'init': 'pca',
    'perplexity': 5,
    'n_iter': 1000,
    'n_iter_without_progress': 300,
    'learning_rate': 200.,
    'n_jobs': -1,
    'random_state': 0
}


def tsne_visualization(data, predicted_labels, tsne_params):
    tsne = manifold.TSNE(**tsne_params)
    tsne_result = tsne.fit_transform(data)
    df_tsne = pd.DataFrame(tsne_result, columns=['tsne_1', 'tsne_2'])
    df_tsne['predicted_labels'] = predicted_labels
    plt.figure(figsize=(15, 12))
    sns.scatterplot(
        x='tsne_1', y='tsne_2',
        hue='predicted_labels',
        palette=sns.color_palette("hls", df_tsne['predicted_labels'].nunique()),
        data=df_tsne,
        legend="full",
        alpha=0.4)
In [44]:
%%time
kmeans_pipe.fit(df)
tsne_visualization(kmeans_pipe['preprocessor'].transform(df), kmeans_pipe['clusterer'].labels_, tsne_params)
CPU times: total: 30min 2s
Wall time: 17min 10s
No description has been provided for this image

5. Interprêtation des clusters

In [45]:
# add predicted labels to the inital dataset
kmeans_pipe.fit(df)
df_labels = df.copy()
df_labels['predicted_label'] = kmeans_pipe['clusterer'].labels_
In [25]:
def clustering_interpretations(df, min_max=(0, 100)):
   
    grouped_df = df.select_dtypes(include=['number']).groupby('predicted_label').mean()
    scaler = MinMaxScaler(min_max)
    scaled_df = pd.DataFrame(scaler.fit_transform(grouped_df), columns=grouped_df.columns)
    scores = []
    for cluster, row in scaled_df.iterrows():
        print(row)
        scores.append(row)
        fig = px.line_polar(row, r=row.values, theta=row.index, line_close=True, title='Cluster {}'.format(cluster))
        fig.update_traces(fill='toself')
        fig.show()
    return pd.DataFrame(scores)
In [50]:
clusters_descr = clustering_interpretations(df_labels)
nb_orders                            67.910687
total_products_ordered              100.000000
total_paid                           37.351216
mean_photo_qty_for_ordered_prods      0.000000
mean_reviews_score                   56.542820
recency                              75.363783
mean_delivery_time                   77.366792
Name: 0, dtype: float64
nb_orders                            85.760076
total_products_ordered               71.408393
total_paid                           27.315241
mean_photo_qty_for_ordered_prods     13.873472
mean_reviews_score                  100.000000
recency                               0.000000
mean_delivery_time                    0.000000
Name: 1, dtype: float64
nb_orders                            47.078506
total_products_ordered               45.214066
total_paid                           63.718499
mean_photo_qty_for_ordered_prods     28.945336
mean_reviews_score                    0.000000
recency                              88.902617
mean_delivery_time                  100.000000
Name: 2, dtype: float64
nb_orders                           100.000000
total_products_ordered               86.890062
total_paid                            0.000000
mean_photo_qty_for_ordered_prods     45.024621
mean_reviews_score                   70.287270
recency                             100.000000
mean_delivery_time                   72.511438
Name: 3, dtype: float64
nb_orders                             0.000000
total_products_ordered                0.000000
total_paid                          100.000000
mean_photo_qty_for_ordered_prods    100.000000
mean_reviews_score                   84.839486
recency                              85.351412
mean_delivery_time                   79.503168
Name: 4, dtype: float64
  • Cluster 0 : Clients fidèles à haute commande
    • Caractéristiques principales :
      • Nombre de commandes : 67.91
      • Produits commandés : 100.00
      • Total payé : 37.35
      • Quantité moyenne de photos pour les produits commandés : 0.00
      • Note moyenne des avis : 56.54
      • Récence : 75.36
      • Temps moyen de livraison : 77.37
    • Stratégies :
      • Fidélisation : Proposer des récompenses pour les achats fréquents, comme des programmes de points ou des cartes de fidélité.
      • Optimisation des livraisons : Améliorer les délais de livraison pour accroître la satisfaction.
  • Cluster 1 : Clients très satisfaits et réguliers
    • Caractéristiques principales :
      • Nombre de commandes : 85.76
      • Produits commandés : 71.41
      • Total payé : 27.32
      • Quantité moyenne de photos pour les produits commandés : 13.87
      • Note moyenne des avis : 100.00 (très élevé)
      • Récence : 0.00 (ancien)
      • Temps moyen de livraison : 0.00
    • Stratégies :
      • Fidélisation : Proposer des offres exclusives pour les inciter à continuer leurs achats.
      • Engagement : Maintenir un contact régulier avec des campagnes de marketing par email.
  • Cluster 2 : Clients à forte dépense mais à faible activité récente
    • Caractéristiques principales :
      • Nombre de commandes : 47.08
      • Produits commandés : 45.21
      • Total payé : 63.72
      • Quantité moyenne de photos pour les produits commandés : 28.95
      • Note moyenne des avis : 0.00
      • Récence : 88.90
      • Temps moyen de livraison : 100.00
    • Stratégies :
      • Réengagement : Utiliser des campagnes de réactivation pour les inciter à revenir, comme des emails ou des notifications push.
      • Incitations : Offrir des réductions ou des avantages pour leur premier achat après une période d'inactivité.
  • Cluster 3 : Clients réguliers mais à faible dépense récente
    • Caractéristiques principales :
      • Nombre de commandes : 100.00
      • Produits commandés : 86.89
      • Total payé : 0.00
      • Quantité moyenne de photos pour les produits commandés : 45.02
      • Note moyenne des avis : 70.29
      • Récence : 100.00
      • Temps moyen de livraison : 72.51
    • Stratégies :
      • Incitations : Offrir des promotions pour augmenter leurs dépenses.
      • Recommandations personnalisées : Proposer des produits basés sur leurs historiques d'achats.
  • Cluster 4 : Clients à haute dépense mais inactifs
    • Caractéristiques principales :
      • Nombre de commandes : 0.00
      • Produits commandés : 0.00
      • Total payé : 100.00 (très élevé)
      • Quantité moyenne de photos pour les produits commandés : 100.00
      • Note moyenne des avis : 84.84
      • Récence : 85.35
      • Temps moyen de livraison : 79.50
    • Stratégies :
      • Réengagement : Utiliser des campagnes de réactivation pour les inciter à revenir, comme des emails ou des notifications push.
      • Incitations : Offrir des réductions ou des avantages pour leur premier achat après une période d'inactivité.

6. Stabilisation du modèle

In [19]:
def test_init_stability(pipeline, data, nb_tests):
    # unfix random_state
    if pipeline['clusterer'].random_state:
        pipeline['clusterer'].random_state = None

    # baseline model
    pipeline.fit(data)
    ref_labels = pipeline['clusterer'].labels_

    tested_models_dict = {}
    for i in range(1, nb_tests+1):
        current_test = {}
        pipeline.fit(data)
        # store current test model
        current_test['model'] = pipeline['clusterer']
        # store ari score
        current_test['ari_score'] = adjusted_rand_score(ref_labels, current_test['model'].labels_)
        # store current test in the dict to return
        tested_models_dict[i] = current_test
        print('Test {} - ARI : {}'.format(i, current_test['ari_score']))
    return tested_models_dict
In [53]:
tested_models = test_init_stability(kmeans_pipe, df, 20)
Test 1 - ARI : 0.21338664296635068
Test 2 - ARI : 0.25900956616931803
Test 3 - ARI : 0.4087918683487351
Test 4 - ARI : 0.5571098367062934
Test 5 - ARI : 0.35960315775069857
Test 6 - ARI : 0.7918331157641595
Test 7 - ARI : 0.3686104795533704
Test 8 - ARI : 0.4626572775244739
Test 9 - ARI : 0.28186317970078656
Test 10 - ARI : 0.3078532577476168
Test 11 - ARI : 0.22034780038828441
Test 12 - ARI : 0.429158284217817
Test 13 - ARI : 0.22391096152908005
Test 14 - ARI : 0.19667435144652914
Test 15 - ARI : 0.32509458119852064
Test 16 - ARI : 0.5125770872684193
Test 17 - ARI : 0.35003854742854923
Test 18 - ARI : 0.35017904227206315
Test 19 - ARI : 0.33417803497923165
Test 20 - ARI : 0.2989457031188324

Optimisation des Hyperparamètres

Avec ce nombre de clusters, quelle est la combinaision des autres hyperparamètres qui permettent de trouver un bon modèle.

In [20]:
from sklearn.model_selection import GridSearchCV
from sklearn.metrics import silhouette_samples, silhouette_score
kmeans_pipe_2 = Pipeline([
    ('preprocessor', preprocessor),
    ('clusterer', KMeans())
])
# Définir la grille des paramètres pour GridSearchCV
# Change parameter names to directly reference the 'clusterer' step
param_grid = {
    'clusterer__n_clusters': [5],
    'clusterer__init': ['k-means++', 'random'],
    'clusterer__n_init': [10, 20, 30],
    'clusterer__max_iter': [300, 400, 500]
}

# Définir une fonction de scoring pour silhouette_score
def silhouette_scorer(estimator, X):
    cluster_labels = estimator.fit_predict(X)
    return silhouette_score(X, cluster_labels)

# Utiliser GridSearchCV pour trouver les meilleurs paramètres
grid_search = GridSearchCV(kmeans_pipe_2, param_grid, cv=5, scoring=silhouette_scorer)
grid_search.fit(df)

# Meilleurs paramètres
best_params = grid_search.best_params_
best_estimator = grid_search.best_estimator_

print("Meilleurs paramètres : ", best_params)
Meilleurs paramètres :  {'clusterer__init': 'k-means++', 'clusterer__max_iter': 300, 'clusterer__n_clusters': 5, 'clusterer__n_init': 10}
In [22]:
best_pipeline = grid_search.best_estimator_
best_pipeline
Out[22]:
Pipeline(steps=[('preprocessor',
                 Pipeline(steps=[('encoding',
                                  OneHotEncoder(cols=['favorite_category',
                                                      'favorite_payment_type',
                                                      'customer_state'])),
                                 ('features_scaling', MinMaxScaler()),
                                 ['pca',
                                  PCA(n_components=26, random_state=0)]])),
                ('clusterer', KMeans(n_clusters=5, n_init=10))])
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
Pipeline(steps=[('preprocessor',
                 Pipeline(steps=[('encoding',
                                  OneHotEncoder(cols=['favorite_category',
                                                      'favorite_payment_type',
                                                      'customer_state'])),
                                 ('features_scaling', MinMaxScaler()),
                                 ['pca',
                                  PCA(n_components=26, random_state=0)]])),
                ('clusterer', KMeans(n_clusters=5, n_init=10))])
Pipeline(steps=[('encoding',
                 OneHotEncoder(cols=['favorite_category',
                                     'favorite_payment_type',
                                     'customer_state'])),
                ('features_scaling', MinMaxScaler()),
                ['pca', PCA(n_components=26, random_state=0)]])
OneHotEncoder(cols=['favorite_category', 'favorite_payment_type',
                    'customer_state'])
MinMaxScaler()
PCA(n_components=26, random_state=0)
KMeans(n_clusters=5, n_init=10)
In [26]:
# add predicted labels to the inital dataset
best_pipeline.fit(df)
df_labels = df.copy()
df_labels['predicted_label'] = best_pipeline['clusterer'].labels_
In [27]:
clusters_descr = clustering_interpretations(df_labels)
nb_orders                            0.000000
total_products_ordered               0.000000
total_paid                          62.844008
mean_photo_qty_for_ordered_prods     0.000000
mean_reviews_score                  89.172582
recency                              0.000000
mean_delivery_time                  56.961438
Name: 0, dtype: float64
nb_orders                           100.000000
total_products_ordered              100.000000
total_paid                            8.277678
mean_photo_qty_for_ordered_prods     35.998537
mean_reviews_score                    3.250982
recency                              73.753592
mean_delivery_time                   61.551125
Name: 1, dtype: float64
nb_orders                            76.213040
total_products_ordered               49.481160
total_paid                            0.000000
mean_photo_qty_for_ordered_prods     70.979928
mean_reviews_score                   40.550728
recency                             100.000000
mean_delivery_time                   76.959111
Name: 2, dtype: float64
nb_orders                             6.297305
total_products_ordered                5.930331
total_paid                          100.000000
mean_photo_qty_for_ordered_prods    100.000000
mean_reviews_score                    0.000000
recency                              84.804203
mean_delivery_time                  100.000000
Name: 3, dtype: float64
nb_orders                            46.197509
total_products_ordered                8.475530
total_paid                           44.743001
mean_photo_qty_for_ordered_prods     95.663399
mean_reviews_score                  100.000000
recency                               9.808986
mean_delivery_time                    0.000000
Name: 4, dtype: float64
  • Cluster 0 : Clients passifs avec dépenses modérées
    • Caractéristiques principales :
      • Nombre de commandes : 0.00
      • Produits commandés : 0.00
      • Total payé : 62.84
      • Quantité moyenne de photos pour les produits commandés : 0.00
      • Note moyenne des avis : 89.17
      • Récence : 0.00 (inactif récemment)
      • Temps moyen de livraison : 56.96
    • Stratégies :
      • Réengagement : Utiliser des campagnes de réactivation pour les inciter à revenir.
      • Offres spéciales : Proposer des promotions pour stimuler les achats.
  • Cluster 1 : Clients réguliers à faible dépense récente
    • Caractéristiques principales :
      • Nombre de commandes : 100.00
      • Produits commandés : 100.00
      • Total payé : 8.28
      • Quantité moyenne de photos pour les produits commandés : 35.99
      • Note moyenne des avis : 3.25
      • Récence : 73.75
      • Temps moyen de livraison : 61.55
    • Stratégies :
      • Fidélisation : Offrir des récompenses pour leurs achats fréquents.
      • Optimisation des avis : Encourager à laisser des avis pour améliorer leur score moyen.
  • Cluster 2 : Clients fidèles à haute commande mais faible dépense récente
    • Caractéristiques principales :
      • Nombre de commandes : 76.21
      • Produits commandés : 49.48
      • Total payé : 0.00
      • Quantité moyenne de photos pour les produits commandés : 70.98
      • Note moyenne des avis : 40.55
      • Récence : 100.00
      • Temps moyen de livraison : 76.96
    • Stratégies :
      • Réengagement : Offrir des promotions pour réactiver les achats.
      • Amélioration de la livraison : Réduire les délais de livraison pour améliorer la satisfaction.
  • Cluster 3 : Clients à haute dépense unique
    • Caractéristiques principales :
      • Nombre de commandes : 6.30
      • Produits commandés : 5.93
      • Total payé : 100.00
      • Quantité moyenne de photos pour les produits commandés : 100.00
      • Note moyenne des avis : 0.00
      • Récence : 84.80
      • Temps moyen de livraison : 100.00
    • Stratégies :
      • Fidélisation : Proposer des offres pour les inciter à effectuer d'autres achats importants.
      • Amélioration de la livraison : Réduire les délais de livraison pour augmenter la satisfaction.
  • Cluster 4 : Clients satisfaits à faible activité
    • Caractéristiques principales :
      • Nombre de commandes : 46.20
      • Produits commandés : 8.48
      • Total payé : 44.74
      • Quantité moyenne de photos pour les produits commandés : 95.66
      • Note moyenne des avis : 100.00
      • Récence : 9.81
      • Temps moyen de livraison : 0.00
    • Stratégies :
      • Réengagement : Utiliser des campagnes de réactivation pour les inciter à revenir.
      • Incitations : Offrir des promotions pour stimuler les achats.
In [58]:
tested_models = test_init_stability(best_pipeline, df, 20)
Test 1 - ARI : 1.0
Test 2 - ARI : 1.0
Test 3 - ARI : 1.0
Test 4 - ARI : 1.0
Test 5 - ARI : 1.0
Test 6 - ARI : 1.0
Test 7 - ARI : 1.0
Test 8 - ARI : 1.0
Test 9 - ARI : 1.0
Test 10 - ARI : 1.0
Test 11 - ARI : 1.0
Test 12 - ARI : 1.0
Test 13 - ARI : 1.0
Test 14 - ARI : 0.6180049538252859
Test 15 - ARI : 1.0
Test 16 - ARI : 0.8541371803007943
Test 17 - ARI : 1.0
Test 18 - ARI : 0.6180049538252859
Test 19 - ARI : 1.0
Test 20 - ARI : 0.5350762786704614
In [59]:
f, ax = plt.subplots(figsize=(10, 5))
ax.plot(np.arange(0, len(tested_models)), [tested_models[x]['ari_score'] for x in tested_models], marker='o')
ax.set_title('ARI score by test')
ax.set_xlabel('Tests')
ax.set_ylabel('ARI score')
ax.set_xticks(np.arange(0, len(tested_models)+1, 1))
ax.set_yticks(np.arange(0, 1.1, 0.1))
plt.show()
No description has been provided for this image
In [60]:
print('Average ARI :', sum([tested_models[x]['ari_score'] for x in tested_models]) / len(tested_models))
Average ARI : 0.9312611683310914

7. Stabilité du modèle dans le temps - Fréquence de mise à jour

In [61]:
min_date = pd.to_datetime(db_df['order_purchase_timestamp'].min()).to_pydatetime()
max_date = pd.to_datetime(db_df['order_purchase_timestamp'].max()).to_pydatetime()
print('The dataset contain data from {} to {}'.format(min_date.strftime('%Y-%m-%d'), max_date.strftime('%Y-%m-%d')))
The dataset contain data from 2016-09-04 to 2018-10-17

Cette fonction test_temporal_stability évalue la stabilité temporelle d'un modèle de clustering intégré dans un pipeline. Elle commence par créer une copie des données fournies et fixe un état aléatoire pour assurer la reproductibilité des résultats si un état aléatoire est spécifié. La période d'analyse commence à partir d'une date donnée ou de la première date disponible dans les données. La fonction utilise les données d'une première année comme référence pour former un modèle initial.

Les caractéristiques des données de référence sont calculées, et le modèle est ajusté avec ces données. Ensuite, mois par mois, des données supplémentaires sont ajoutées, et le modèle est réajusté à chaque étape. Après chaque ajustement, les étiquettes des clients du modèle de référence sont prédites avec les nouvelles données, et le score ARI (Adjusted Rand Index) est calculé pour mesurer la stabilité des clusters par rapport au modèle de référence initial.

Les résultats, comprenant les modèles ajustés et les scores ARI, sont stockés dans un dictionnaire. Le score ARI indique dans quelle mesure les clusters restent cohérents avec le temps, ce qui permet de déterminer la fréquence de mise à jour nécessaire du modèle pour maintenir une performance optimale. Cette méthode permet ainsi de surveiller et d'assurer la robustesse du modèle de clustering face à l'évolution des données.

In [62]:
def test_temporal_stability(pipeline, db_df, begin_at=None, random_state=None, rfm=False):
    final_dict = {}  # To store models and scores
    data = db_df.copy()

    # fix a random_state to help
    if not pipeline['clusterer'].random_state and random_state:
        pipeline['clusterer'].random_state = random_state

    if begin_at:
        min_date = pd.to_datetime(begin_at)
    min_date = pd.to_datetime(data['order_purchase_timestamp'].min()).to_pydatetime()
    max_date = pd.to_datetime(data['order_purchase_timestamp'].max()).to_pydatetime()
    limit = min_date + relativedelta(years=+1)

    # get the first year of data
    ref_data = data[(data['order_purchase_timestamp'] >= min_date) & (data['order_purchase_timestamp'] < limit)]
    # compute all features
    ref_data = compute_features(ref_data.copy())
    # store the unique ids of the ref customers
    ref_cust_ids = ref_data.index
    if rfm:
        ref_data = ref_data[['recency', 'nb_orders', 'total_paid']]

    # ref model
    pipeline.fit(ref_data)
    final_dict['ref'] = pipeline['clusterer']
    ref_data_pca = pipeline['preprocessor'].transform(ref_data)
    ref_model_labels = pipeline['clusterer'].labels_
    print('Ref data: {} rows'.format(ref_data.shape[0]))

    # Adds data of additional months until the end and train a model each time
    i = 1  # test number
    while limit < max_date:
        # increase limit by 1 month (placed here to get all data even if the last month is not complete)
        limit += relativedelta(months=+1)
        # get data from the min date to the limit date
        cur_data = data[(data['order_purchase_timestamp'] >= min_date) & (data['order_purchase_timestamp'] < limit)]
        cur_data = compute_features(cur_data.copy())
        if rfm:
            cur_data = cur_data[['recency', 'nb_orders', 'total_paid']]
        cur_cust_nb = cur_data.shape[0]
        # fit model on all current data
        pipeline.fit(cur_data)
        # get rid of customers not in the ref model
        cur_data = cur_data[cur_data.index.isin(ref_cust_ids)]
        # predict labels of clients in the ref model (with their data updated)
        cur_model_labels = pipeline.predict(cur_data)
        # store current model
        final_dict[i] = {'model': pipeline['clusterer']}
        # predict labels for customers in ref data
        cur_model_labels = pipeline['clusterer'].predict(ref_data_pca)
        # compute ARI score
        final_dict[i]['ari_score'] = adjusted_rand_score(ref_model_labels, cur_model_labels)
        print('Ref data + {} months ({} rows): ARI = {} '.format(i, cur_cust_nb, final_dict[i]['ari_score']))
        i += 1

    return final_dict
In [63]:
temp_stab_dict = test_temporal_stability(kmeans_pipe, db_df, random_state=1)
Ref data: 23138 rows
Ref data + 1 months (27351 rows): ARI = 0.48122767112792747 
Ref data + 2 months (31696 rows): ARI = 0.3215179521467082 
Ref data + 3 months (39503 rows): ARI = 0.6575759833281585 
Ref data + 4 months (44731 rows): ARI = 0.5874430469601531 
Ref data + 5 months (51820 rows): ARI = 0.32671750933207966 
Ref data + 6 months (58414 rows): ARI = 0.47070841878674946 
Ref data + 7 months (65411 rows): ARI = 0.2952388056524625 
Ref data + 8 months (72234 rows): ARI = 0.5612280304306734 
Ref data + 9 months (78486 rows): ARI = 0.41197864613319163 
Ref data + 10 months (84520 rows): ARI = 0.505904068292167 
Ref data + 11 months (90911 rows): ARI = 0.5091041075873413 
Ref data + 12 months (96091 rows): ARI = 0.9903698161395945 
Ref data + 13 months (96095 rows): ARI = 0.2741040702988693 
Ref data + 14 months (96096 rows): ARI = 0.6461730696608041 
In [64]:
temp_stab_dict = test_temporal_stability(best_pipeline, db_df, random_state=1)
Ref data: 23138 rows
Ref data + 1 months (27351 rows): ARI = 1.0 
Ref data + 2 months (31696 rows): ARI = 0.5238007193013058 
Ref data + 3 months (39503 rows): ARI = 0.9923447379790604 
Ref data + 4 months (44731 rows): ARI = 0.99298057164426 
Ref data + 5 months (51820 rows): ARI = 0.5257365018892318 
Ref data + 6 months (58414 rows): ARI = 0.983773652512818 
Ref data + 7 months (65411 rows): ARI = 0.9829029913862524 
Ref data + 8 months (72234 rows): ARI = 0.9942533916458502 
Ref data + 9 months (78486 rows): ARI = 0.9942533916458502 
Ref data + 10 months (84520 rows): ARI = 0.9943807580560333 
Ref data + 11 months (90911 rows): ARI = 0.5553112768756359 
Ref data + 12 months (96091 rows): ARI = 0.9954002409122074 
Ref data + 13 months (96095 rows): ARI = 0.5130918018502667 
Ref data + 14 months (96096 rows): ARI = 0.9952727519678491 

Les scores ARI montrent une forte stabilité du modèle pour plusieurs mois, mais des baisses significatives apparaissent après 2, 5, 11, et 13 mois. Pour maintenir la fiabilité, il est recommandé de mettre à jour le modèle tous les 1 à mois.

In [65]:
f, ax = plt.subplots(figsize=(10, 5))
ax.plot(np.arange(1, len(temp_stab_dict)), [temp_stab_dict[x]['ari_score'] for x in temp_stab_dict if x != 'ref'], marker='o')
ax.set_title('ARI score evolution in time')
ax.set_xlabel('Additional months from baseline')
ax.set_ylabel('ARI score')
ax.set_xticks(np.arange(1, len(temp_stab_dict), 1))
ax.set_yticks(np.arange(0, 1.2, 0.1))
plt.show()
No description has been provided for this image

B. K-Means avec méthode RFM

In [67]:
kmeans_rfm_pipe = Pipeline([
    ('preprocessor', StandardScaler()),
    ('clusterer', KMeans())
])
In [68]:
df_rfm_kmeans = df[['recency', 'total_paid', 'nb_orders']].copy()

1. Nombre optimal de clusters

In [69]:
k_range = (2, 11)
display_elbow_visu(kmeans_rfm_pipe, df_rfm_kmeans, k_range, 'distortion')
No description has been provided for this image
In [70]:
display_elbow_visu(kmeans_rfm_pipe, df_rfm_kmeans, k_range, 'silhouette')
No description has been provided for this image
In [71]:
kmeans_rfm_pipe['clusterer'].n_clusters = 4

2. Visualisation

In [72]:
kmeans_rfm_pipe.fit(df_rfm_kmeans)
Out[72]:
Pipeline(steps=[('preprocessor', StandardScaler()),
                ('clusterer', KMeans(n_clusters=4))])
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
Pipeline(steps=[('preprocessor', StandardScaler()),
                ('clusterer', KMeans(n_clusters=4))])
StandardScaler()
KMeans(n_clusters=4)
In [76]:
df_rfm_kmeans_labels = df_rfm_kmeans.copy()
df_rfm_kmeans_labels['predicted_label'] = kmeans_rfm_pipe['clusterer'].labels_
rfm_iso_scatter(df_rfm_kmeans_labels, 'recency', 'nb_orders', 'total_paid', color='predicted_label', reduce=True)
In [77]:
%%time
tsne_visualization(kmeans_rfm_pipe['preprocessor'].transform(df_rfm_kmeans), kmeans_rfm_pipe['clusterer'].labels_, tsne_params)
CPU times: total: 31min 16s
Wall time: 16min 55s
No description has been provided for this image

3. Interprêtation des clusters

In [78]:
clusters_descr_rfm = clustering_interpretations(df_rfm_kmeans_labels, (0, 4))
recency       4.000000
total_paid    0.002496
nb_orders     0.000000
Name: 0, dtype: float64
recency       1.755021
total_paid    4.000000
nb_orders     0.086754
Name: 1, dtype: float64
recency       0.0
total_paid    0.0
nb_orders     0.0
Name: 2, dtype: float64
recency       1.390199
total_paid    0.437025
nb_orders     4.000000
Name: 3, dtype: float64
  • Cluster 0 : Clients inactifs
    • Caractéristiques principales :
      • Récence : 4.00 (ancien)
      • Total payé : 0.002496
      • Nombre de commandes (nb_orders) : 0.00
    • Stratégies :
      • Réactivation : Offrir des incitations pour revenir, telles que des coupons ou des promotions.
      • Engagement : Envoyer des campagnes de marketing par email pour susciter l'intérêt.
  • Cluster 1 : Clients à forte dépense
    • Caractéristiques principales :
      • Récence : 1.76
      • Total payé : 4.00 (très élevé)
      • Nombre de commandes (nb_orders) : 0.09
    • Stratégies :
      • Programmes de fidélité : Proposer des récompenses exclusives pour les inciter à continuer leurs achats.
      • Offres personnalisées : Envoyer des offres basées sur leurs historiques d'achats pour maximiser l'engagement.
  • Cluster 2 : Clients très inactifs
    • Caractéristiques principales :
      • Récence : 0.00
      • Total payé : 0.00
      • Nombre de commandes (nb_orders) : 0.00
    • Stratégies :
      • Réengagement : Utiliser des campagnes de réactivation pour les inciter à revenir, comme des emails ou des notifications push.
      • Incitations : Offrir des réductions ou des avantages pour leur premier achat après une période d'inactivité.
  • Cluster 3 : Clients fréquents
    • Caractéristiques principales :
      • Récence : 1.39
      • Total payé : 0.44
      • Nombre de commandes (nb_orders) : 4.00 (très élevé)
    • Stratégies :
      • Fidélisation : Offrir des récompenses pour les achats fréquents, comme des programmes de points ou des cartes de fidélité.
      • Personnalisation : Proposer des recommandations de produits basées sur leurs habitudes d'achat pour maximiser leur satisfaction.
In [ ]:
clusters_label_rfm = {
    0: 'Lost customers, low spenders',
    1: 'New customers, low spenders',
    2: 'Best customers, loyalists'
}
df_rfm_kmeans_labels['cluster_interpretation'] = df_rfm_kmeans_labels['predicted_label'].apply(lambda x: clusters_label_rfm[x])
In [ ]:
df_recap_kmeans_rfm = df_rfm_kmeans_labels.reset_index().groupby('predicted_label').agg(
    Count=('customer_unique_id', 'count'),
    Recency=('recency', 'mean'),
    Frequency=('nb_orders', 'mean'),
    Monetary=('total_paid', 'mean'),
    ClusterInterpretation=('cluster_interpretation', 'unique')
).round(1)
df_recap_kmeans_rfm

4. Stabilité du modèle

In [79]:
tested_models = test_init_stability(kmeans_rfm_pipe, df_rfm_kmeans, 10)
f, ax = plt.subplots(figsize=(10, 5))
ax.plot(np.arange(0, len(tested_models)), [tested_models[x]['ari_score'] for x in tested_models], marker='o')
ax.set_title('ARI score evolution')
ax.set_xlabel('Tests')
ax.set_ylabel('ARI score')
ax.set_xticks(np.arange(0, len(tested_models)+1, 1))
ax.set_yticks(np.arange(0, 1.2, 0.1))
plt.show()
Test 1 - ARI : 0.9653289329612601
Test 2 - ARI : 0.9703162644519343
Test 3 - ARI : 0.9699059488929386
Test 4 - ARI : 0.9564521879590366
Test 5 - ARI : 0.9703162644519343
Test 6 - ARI : 0.9699059488929386
Test 7 - ARI : 0.9699059488929386
Test 8 - ARI : 0.9703162644519343
Test 9 - ARI : 0.9703162644519343
Test 10 - ARI : 0.9644294770078878
No description has been provided for this image

Ces résultats indiquent une stabilité générale du modèle avec des scores ARI élevés et constants, principalement autour de 0.97. Les légères variations observées, comme une baisse à 0.9565 lors du Test 4 et à 0.9644 lors du Test 10, montrent que les clusters restent très cohérents malgré l'ajout de nouvelles données. La constance des scores ARI proches de 0.97 suggère que le modèle est fiable et maintient une bonne stabilité au fil du temps.

5. Fréquence de mise à jour du modèle

In [80]:
temp_stab_dict_rfm = test_temporal_stability(kmeans_rfm_pipe, db_df, random_state=15, rfm=True)
Ref data: 23138 rows
Ref data + 1 months (27351 rows): ARI = 0.9324223274156803 
Ref data + 2 months (31696 rows): ARI = 0.9343698091101679 
Ref data + 3 months (39503 rows): ARI = 0.9780179523856091 
Ref data + 4 months (44731 rows): ARI = 0.9938465222062125 
Ref data + 5 months (51820 rows): ARI = 0.9891774676981612 
Ref data + 6 months (58414 rows): ARI = 0.9644491109587955 
Ref data + 7 months (65411 rows): ARI = 0.9271382850915354 
Ref data + 8 months (72234 rows): ARI = 0.907835046786118 
Ref data + 9 months (78486 rows): ARI = 0.9075800923671268 
Ref data + 10 months (84520 rows): ARI = 0.8906574968524584 
Ref data + 11 months (90911 rows): ARI = 0.890771437294046 
Ref data + 12 months (96091 rows): ARI = 0.9488063885449789 
Ref data + 13 months (96095 rows): ARI = 0.4120834175020492 
Ref data + 14 months (96096 rows): ARI = 0.9286967699968822 

Les résultats montrent les scores ARI (Adjusted Rand Index) pour chaque mois additionnel de données par rapport au modèle de référence.

  • Ref data (23138 rows) : Ce sont les données de référence utilisées pour le modèle initial.
  • Ref data + 1 mois à +12 mois : Les scores ARI restent relativement élevés, variant entre 0.89 et 0.99, indiquant une forte stabilité des clusters malgré l'ajout de nouvelles données. Ceci montre que le modèle reste fiable pour ces périodes.
  • Ref data + 13 mois (96095 rows) : Il y a une chute significative du score ARI à 0.412, indiquant une perte majeure de stabilité du modèle. Cela signifie que les clusters ont changé de manière significative après 13 mois.
  • Ref data + 14 mois (96096 rows) : Le score ARI remonte à 0.928, ce qui est surprenant après la chute drastique du mois précédent. Cela pourrait indiquer une anomalie dans les données ou un réajustement inattendu des clusters.

D'après ces observations, il est recommandé de renouveler le modèle tous les 12 mois pour maintenir une bonne stabilité des clusters.

In [81]:
f, ax = plt.subplots(figsize=(10, 5))
ax.plot(np.arange(1, len(temp_stab_dict_rfm)), [temp_stab_dict_rfm[x]['ari_score'] for x in temp_stab_dict_rfm if x != 'ref'], marker='o')
ax.set_title('ARI score evolution in time')
ax.set_xlabel('Additional months from baseline')
ax.set_ylabel('ARI score')
ax.set_xticks(np.arange(1, len(temp_stab_dict_rfm), 1))
ax.set_yticks(np.arange(0, 1.2, 0.1))
plt.show()
No description has been provided for this image

C. Comparaison de deux K-Means

En comparant les deux modèles, on se rend compte que le modèle avec RFM est meilleur car il est plus stable. Le modèle RFM utilise les variables de recency, de nombre de commandes (nb_orders) et de montant total payé (total_paid), ce qui permet de mieux capturer le comportement d'achat des clients en termes de fréquence, de récence et de montant dépensé. Ces critères sont essentiels pour segmenter les clients en fonction de leur valeur et de leur engagement avec l'entreprise.

En revanche, le modèle sans RFM utilise une gamme plus large de variables, y compris le nombre total de produits commandés (total_products_ordered), la quantité moyenne de photos par produit commandé (mean_photo_qty_for_ordered_prods), la note moyenne des avis (mean_reviews_score) et le temps moyen de livraison (mean_delivery_time). Bien que ces variables fournissent une vue plus détaillée et diversifiée du comportement des clients, elles peuvent introduire plus de variabilité et complexité, rendant le modèle moins stable.

Cependant, il serait difficile de les comparer directement étant donné que les variables ne sont pas les mêmes et que chaque méthode a un objectif défini. Le modèle RFM est spécifiquement conçu pour les analyses de valeur client, tandis que le modèle sans RFM peut offrir des insights plus détaillés mais potentiellement moins cohérents en termes de segmentation stable des clients.

II. Gaussian Mixture Models

Le Gaussian Mixture Model (GMM) est une technique avancée de clustering qui repose sur le principe de mélange gaussien. Ce modèle suppose que les données proviennent d'une combinaison de plusieurs distributions normales (ou gaussiennes), chacune correspondant à un cluster distinct. Chaque distribution est définie par ses propres paramètres : moyenne, covariance et proportion. Le but du GMM est d'estimer ces paramètres de manière à mieux représenter la distribution globale des données.

Contrairement à des méthodes comme K-means, qui attribue chaque point de données à un cluster unique en se basant sur la distance euclidienne, le GMM offre une approche plus flexible et probabiliste. Il permet de modéliser des clusters de formes ellipsoïdales et non simplement sphériques, ce qui le rend particulièrement utile pour des ensembles de données où les clusters ont des formes et tailles variées.

Le GMM utilise un algorithme d'espérance-maximisation (EM) pour ajuster les paramètres des distributions gaussiennes. L'algorithme fonctionne en deux étapes :

  • Étape d'espérance (E-step) : Calcul des probabilités d'appartenance de chaque point de données à chaque composant (distribution) du mélange.
  • Étape de maximisation (M-step) : Mise à jour des paramètres des distributions en maximisant la probabilité que les données observées proviennent de ce modèle.

Ce processus itératif continue jusqu'à ce que la convergence soit atteinte, c'est-à-dire que les changements dans les paramètres deviennent négligeables. En fin de compte, chaque point de données est attribué à un cluster avec une certaine probabilité, permettant une classification plus nuancée et potentiellement plus précise des données complexes.

Le GMM est couramment utilisé dans divers domaines, notamment la reconnaissance de formes, la bioinformatique, l'analyse de texte, et partout où une modélisation flexible des données est nécessaire.

https://towardsdatascience.com/gaussian-mixture-models-vs-k-means-which-one-to-choose-62f2736025f0

A. Clustering

In [ ]:
gmm_params = {
    'n_components': kmeans_pipe['clusterer'].n_clusters,
    'max_iter': 1000,
    'tol': 1e-4,
    'init_params': 'kmeans',
    'random_state': 1
}

gmm_pipe = Pipeline([
    ('preprocessor', preprocessor),
    ('clusterer', GaussianMixture(**gmm_params))
])

gmm_pipe.fit(df)
preprocessed_data_gmm = gmm_pipe['preprocessor'].transform(df)
predicted_labels_gmm = gmm_pipe['clusterer'].predict(preprocessed_data_gmm)

print('Nb iterations to converge: ', gmm_pipe['clusterer'].n_iter_)
print('Silouhette score:', silhouette_score(preprocessed_data_gmm, predicted_labels_gmm))
In [90]:
kmeans_pipe.fit(df)
print(f"Comparison with kmeans predictions : ARI={adjusted_rand_score(kmeans_pipe['clusterer'].labels_, predicted_labels_gmm)}")
Comparison with kmeans predictions : ARI=0.7488953672558403

B. Visualisation

In [95]:
tsne_visualization(preprocessed_data_gmm, predicted_labels_gmm, tsne_params)
No description has been provided for this image
In [92]:
df_labels_gmm = df.copy()
df_labels_gmm['predicted_label'] = predicted_labels_gmm
clustering_interpretations(df_labels_gmm)
nb_orders                            5.365896
total_products_ordered               8.127012
total_paid                          93.597394
mean_photo_qty_for_ordered_prods     0.000000
mean_reviews_score                  85.781933
recency                              0.000000
mean_delivery_time                  16.160215
Name: 0, dtype: float64
nb_orders                           100.000000
total_products_ordered               79.577252
total_paid                            0.000000
mean_photo_qty_for_ordered_prods     64.516846
mean_reviews_score                   47.696830
recency                              72.687194
mean_delivery_time                  100.000000
Name: 1, dtype: float64
nb_orders                           48.885116
total_products_ordered               7.007017
total_paid                          79.724968
mean_photo_qty_for_ordered_prods    27.712337
mean_reviews_score                   0.000000
recency                             15.291593
mean_delivery_time                   9.363278
Name: 2, dtype: float64
nb_orders                            88.658089
total_products_ordered              100.000000
total_paid                           86.304833
mean_photo_qty_for_ordered_prods     85.810186
mean_reviews_score                   34.576523
recency                              33.951955
mean_delivery_time                   50.365285
Name: 3, dtype: float64
nb_orders                             0.0
total_products_ordered                0.0
total_paid                          100.0
mean_photo_qty_for_ordered_prods    100.0
mean_reviews_score                  100.0
recency                             100.0
mean_delivery_time                    0.0
Name: 4, dtype: float64
Out[92]:
nb_orders total_products_ordered total_paid mean_photo_qty_for_ordered_prods mean_reviews_score recency mean_delivery_time
0 5.365896 8.127012 93.597394 0.000000 85.781933 0.000000 16.160215
1 100.000000 79.577252 0.000000 64.516846 47.696830 72.687194 100.000000
2 48.885116 7.007017 79.724968 27.712337 0.000000 15.291593 9.363278
3 88.658089 100.000000 86.304833 85.810186 34.576523 33.951955 50.365285
4 0.000000 0.000000 100.000000 100.000000 100.000000 100.000000 0.000000
  • Cluster 0 : Clients récents et satisfaits
    • Caractéristiques principales :
      • Nombre de commandes (nb_orders) : 5.37
      • Total des produits commandés : 8.13
      • Total payé : 93.60
      • Score moyen des avis : 85.78
      • Récence (recency) : 0.00 (très récent)
      • Délai moyen de livraison : 16.16 jours
    • Stratégies :
      • Fidélisation : Offrir des programmes de fidélité pour encourager les achats répétés.
      • Promotions ciblées : Envoyer des promotions sur les nouveaux produits pour maintenir leur intérêt.
      • Feedback : Solliciter des avis pour continuer à améliorer l'expérience client.
  • Cluster 1 : Acheteurs fréquents à faible dépense
    • Caractéristiques principales :
      • Nombre de commandes (nb_orders) : 100.00 (très élevé)
      • Total des produits commandés : 79.58
      • Total payé : 0.00 (très faible)
      • Score moyen des avis : 47.70
      • Récence : 72.69
      • Délai moyen de livraison : 100.00 jours
    • Stratégies :
      • Augmentation du panier moyen : Proposer des ventes croisées et des offres groupées pour augmenter la dépense moyenne.
      • Amélioration de la satisfaction : Réduire le délai de livraison et améliorer la qualité des produits.
      • Avis et retours : Analyser les avis pour identifier les points d'amélioration.
  • Cluster 2 : Clients occasionnels avec avis neutres
    • Caractéristiques principales :
      • Nombre de commandes (nb_orders) : 48.89
      • Total des produits commandés : 7.01
      • Total payé : 79.72
      • Score moyen des avis : 0.00 (aucun avis donné)
      • Récence : 15.29
      • Délai moyen de livraison : 9.36 jours
    • Stratégies :
      • Engagement : Encourager les avis clients pour obtenir des retours.
      • Re-marketing : Utiliser des campagnes de re-marketing pour les inciter à revenir.
      • Expérience client : Améliorer l'expérience d'achat pour transformer ces clients en acheteurs réguliers.
  • Cluster 3 : Clients actifs et diversifiés
    • Caractéristiques principales :
      • Nombre de commandes (nb_orders) : 88.66
      • Total des produits commandés : 100.00
      • Total payé : 86.30
      • Score moyen des avis : 34.58
      • Récence : 33.95
      • Délai moyen de livraison : 50.37 jours
    • Stratégies :
      • Programme de fidélité : Créer des programmes de fidélité pour renforcer leur engagement.
      • Amélioration de la livraison : Réduire les délais de livraison pour améliorer la satisfaction.
      • Personnalisation des offres : Proposer des offres personnalisées basées sur leurs habitudes d'achat.
  • Cluster 4 : Clients premium et exigeants
    • Caractéristiques principales :
      • Nombre de commandes (nb_orders) : 0.00
      • Total des produits commandés : 0.00
      • Total payé : 100.00
      • Score moyen des avis : 100.00
      • Récence : 100.00
      • Délai moyen de livraison : 0.00
    • Stratégies :
      • Maintien de la qualité : Assurer un niveau de service élevé pour répondre à leurs attentes.
      • Offres exclusives : Offrir des produits exclusifs et des expériences premium.
      • Feedback proactif : Obtenir des retours détaillés pour continuer à s'améliorer.
In [ ]: